""" Defines tileset-related classes and functions.

    TODO: Create to_json and from_json for each class defined here.
    TODO: Create a from_json module function that can generate instances of
          any of the TileList classes.
"""

try:
    import json
except ImportError:
    try:
        import simplejson as json
    except ImportError:
        pass

from pygame import Rect, Surface

from sbak import debug
from sbak import error
from sbak import group
from sbak import resman

from sbak.draw import image as image_draw

__all__ = (
    'Tile',
    'TileList',
    'TileSheet',
    'from_json',
    'load'
)


# ~~~~~~~~ CLASSES ~~~~~~~~

class Tile(group.DynamicSprite, resman.Representable):
    """ Basic tile. A sublass of pygame.sprite.Sprite.
        
        Tile objects are not considered resources in and of themselves. They
        are actually just a part of a TileSet object. They can not be shared
        between tilesets.
        
        Any drawable object can be used to represent a tile. (See the `image`
        property.) The first argument to the tile constructor is the image to
        use.
        
        A tile can have any number of "tile attributes." The meaning of these
        attributes is, for the most part, determined by the programmer. They can
        be used for any purpose.
        
        Since a tile is a subclass of Sprite, it can also be added to any number
        of groups. The Tile class supports DynamicGroups, so it can be added
        to a DynamicGroup by giving the ID of the group. A tile is always added
        to the "tile" DynamicGroup when it is created.
    """
    
    tag = "tile"
    
    def __init__(self, image=None, groups=None, tattrs=None):
        self._idn = -1
        group.DynamicSprite.__init__(groups if groups is not None else [])
        self.add('tile')
        self._attr = None
        if tattrs is not None:
            for k,v in tattrs.iteritems():
                self._attr[k] = v
        self.image = image
        self._tlist = None
    
    def __repr__(self):
        return "<Tile %s>"%(self._idn if self._idn >= 0 else '(no IDN)')
    
    def to_json(self, resmgr=None, usenone=False):
        """ Return a JSON representation of the Tile.
        
            resmgr: A resource manager to use to find filenames of
                    values. If None, the global resource manager is
                    used.
        
            This will return an object of the form:
            
            ~~~ TILE JSON FORMAT ~~~
            
            {
                "attrs" : {
                    <str> : <object>,
                    <str> : <object>,
                    ...
                },
                "groups" : [<str>, <str>, ...]
                "image"  : <null | str | object>
            }
            
            ~~~ TILE JSON VALUES ~~~
            
            attrs:  An object mapping tattr keys to tattr values.
            
            groups: A list of the IDs of all groups that the tile is a
                    member of.
            
            image:  The image that the tile is using. If the image was
                    loaded with the given resource manager, this will
                    be the filename of the 
        """
        if resmgr is None:
            resmgr = resman.get_mgr()
        
        d = {
            "tattrs" : self._attr,
            "image" : resmgr.res_to_json(self.image, usenone, True),
            "groups" : [g.id for g in groups if hasattr(g, 'id')]
        }
        
        return d
    
    def draw(self,surf=None,pos=(0,0),region=None):
        """ Draws the tile on the given surface at the given position.
            If surf is None the screen surface will be used.
        """
        if self._image is not None:
            image_draw(self._image,surf,pos,region)

    def set_tattr(self,key,val):
        """ Use to set the value of a tile attribute.
        
            TODO: Only allow strings and numbers.
        """
        self._attr[key] = val
    
    def get_tattr(self,key):
        """ Use to get the value of a tile attribute. """
        return self._attr[key]
    
    def del_tattr(self,key):
        """ Use to remove a tile attribute. """
        del self._attr[key]
    
    _rep_tattrs = lambda self: self._attr
        
    def set_image(self,image):
        if isinstance(image,str):
            image = res.get(image)
        self._image = image
    image = property(lambda self: self._image, set_image, None,
        """ The drawable object used to draw this tile.
            May be set to a resource filename. However, the value of this
            property will become the actual resource, not the filename.
        """)
    _rep_image = lambda self, m, n: m.res_to_json(self.image, n, True),
    _unrep_image = lambda self, v, m: image.from_json(v, m)
    
    def get_tlist(self):
        return self._tlist
    tlist = property(get_tlist,None,None,
        """ The tilelist that a tile belongs to. (Read only.)
            Note that if you want to add a tile to a tileset you must use the
            tileset's append() method.
        """)
    
    def _set_tlist(self,tlist,idn):
        """ Internal function.
            Used by tilelists that are adding the tile to themselves.
        """
        t = self._tlist
        if t is not None and t is not tlist:
            t._remove(self)
        self._tlist = tlist
        self._idn = idn
    
    idn = property(lambda self: self._idn,None,None,
        " The index number of this tile within the tilelist. "
        "(Read only.)")

class TileList(list, resman.Resource):
    """ A basic TileList.
        
        A TileList, as the name implies, is a list specialized for the
        use of Tile objects.
        
        Supports all list operations except slicing.
        
        (TODO: add slicing support.)
    """
    
    tag = "tilelist"
    
    def __init__(self,tiles=None):
        """ Initializes a TileList.
            Optionally pass a sequence of Tile objects.
        """
        # This must exist to be handleed correctly by tilemaps.
        self._is_tilelist = True 
        
        if tiles:
            for t in tiles:
                self.append(t)
    
    @classmethod
    def from_json(cls, obj, resmgr=None):
        """ Create a TileList object from a JSON representation.
        
            obj:    A JSON representation of a TileList.
            
            resmgr: A resource managaer to use to create Tiles. If
                    None, the global resource manager will be used.
            
            Returns a TileList object.
        """
        if resmgr is None:
            resmgr = resman.get_mgr()
        
        d = dict(obj)
        del obj
        
        if "tag" in d:
            del d["tag"]
        
        if "tiles" in d:
            d["tiles"] = []
            for t in tiles:
                d["tiles"].append(Tile.from_json(t, resmgr))
        
        tlist = cls(**d)
        
        return tlist
    
    def __repr__(self):
        return "TileList(%s)"%(list.__repr__(self))
    
    def to_json(self, resmgr=None, usenone=False):
        """ Creates a JSON representation of a TileList object.
        
            resmgr:     A resource manager to use to represent resource
                        components of tiles in the tile list. If None,
                        the global resource manager is used.
            
            usenone:    If True, tiles that cannot be represented will
                        be represented with None (JSON 'null').
                        otherwise, sbak.error.RepresentError will be
                        raised when encountering such an object.
            
            ~~~ JSON Format ~~~
            
            {
                "tag" : "tilelist,
                "tiles" : [
                    <Tile>,
                    <Tile>,
                    ...
                ]
            }
            
            ~~~ Format Explanation ~~~
            
            "tag":      The tag for a TileList is always "tilelist".
            "tiles":    A list (JSON array) of representations of Tile
                        objects created from the tiles in the TileList.
            
            Returns a JSON representation of a TileList object.
        """
        if resmgr is None:
            resmgr = resman.get_mgr()
        
        d = {
            "tag": self.tag,
            "tiles": [t.to_json(resmgr, usenone) for t in self]
        }
        
        return d
    
    def append(self,t):
        """ Add a tile to this.
            This will remove the tile from its current tileset.
        """
        t._set_tlist(self,len(self))
        list.append(self,t)
    
    def remove(self,t):
        """ Remove a tile. """
        t._set_tlist(None,None)
        list.remove(self,t)
        
    def _remove(self,t):
        """ Internal function.
            Used by tiles removing themselves from a tilelist.
        """
        list.remove(self,t)
    
    def create_tile(self,sprite=None,*groups,**tattrs):
        """ Creates a new tile and appends it to the TileList.
            Returns a Tile object.
        """
        t = Tile(sprite,*groups,**tattrs)
        self.append(t)
        return t
    
    def __setitem__(self,k,v):
        """ FIXME: Does not support slicing. """
        self[k]._set_tlist(self,k)
        list.__setitem__(self,k,v)

class TileSheet(TileList):
    """ A TileList that gets surfaces of all tiles from one surface.
        
        A TileSheet is the same as a TileList except that it has the
        capacity to create new tiles by cutting out rectangular regions
        of a surface. These cutouts are subsurfaces of the initial
        surface. The create_tile_n function is used for this.
    """
    
    __slots__ = (
        '_surf',   # Shared surface
        '_region', # Offset of each tile
        '_sep',    # Tuple of x- and y-separation pixels between tiles
        '_cols',   # Number of colums in a surface
        '_sub'     # Tuple of subsurfaces created from shared surface
    )
    
    def __init__(self,surf,region,sep=(0,0),tiles=tuple()):
        """ Initializes a TileSheet.
            
            surf:   Pass a pygame surface. This will be used to create
                    subsurfaces for tiles in the set.
            region: Pass a Pygame Rect or compatible. The size is used
                    as the size of each subsurface. The position is
                    used as an offset from the top-left corner of the
                    surface.
            sep:    Pass a sequence of any 2 integers. These are
                    additionals offsets to place between each tile
                    subsurface.
        """
        if not isinstance(surf,Surface):
            raise TypeError("surf must be pygame.Surface, not '%s'"%type(surf))
        self._surf = surf
        self._region = Rect(region)
        for v in sep:
            if not isinstance(v,int):
                raise TypeError("sep must be sequence of 2 integers.")
        self._sep = sep
        self._cols = 0
        self._sub = tuple()
        self._gen_subs()
        super(self.__class__,self).__init__(self,tiles)
        
    def __repr__(self):
        return "TileSheet(%s)"%list.__repr__(self)
    
    def create_tile_n(self,n,groups=None,tattrs=None):
        """ Creates a new tile at the end of the tilesheet.
            
            This tile will have a sprite that is one of the subsurfaces
            created from the shared surface of the tilesheet as denoted
            by `n`.
            
            The tile is appended immediately.
            
            Returns a new Tile object.
        """
        return TileList.create_tile(self,self._sub[n],groups,tattrs)
    
    def _gen_subs(self):
        """ Internal function.
            (Re)generates all subimages for current settings.
        """
        # Calculate columns and rows for subsurfaces
        surf = self._surf
        surfW,surfH = surf.get_size()
        offX,offY,tileW,tileH = self._region
        sepX,sepY = self._sep
        cols,rows = surfW/(tileW+sepX), surfH/(tileH+sepY)
        # Generate the subsurfaces
        subs = []
        for row in xrange(rows):
            for col in xrange(cols):
                region = (
                    offX+(tileW+sepX)*col,
                    offY+(tileH+sepY)*row,
                    tileW, tileH
                )
                sub = surf.subsurface(region)
                subs.append(sub)
        # Store the new subsurfaces
        self._sub = tuple(subs)

    def set_surface(self,surf):
        if not isinstance(surf,Surface):
            raise TypeError("surf must be pygame.Surface, not '%s'"%type(surf))
        self._surf = surf
        self._gen_subs()
    surface = property(
        fget = lambda self: self._surf,
        fset = set_surface,
        doc =
        """ The shared surface of the tilesheet.
            Changing this will update the sub surface list.
        """)
    
    def get_offset_x(self):
        return self._region[0]
    def set_offset_x(self,ox):
        self._region[0] = ox
        self._gen_subs()
    offset_x = property(get_offset_x,set_offset_x,None,
        """ The relative x offset for all subsurfaces.
            Changing this will update the list of subsurfaces.
        """)
    
    def get_offset_y(self):
        return self._region[1]
    def set_offset_y(self,oy):
        self._region[1] = oy
        self._gen_subs()
    offset_y = property(get_offset_y,set_offset_y,None,
        """ The relative y offset for all subsurfaces.
            Changing this will update the list of subsurfaces.
        """)
    
    def get_tile_w(self):
        return self._region[2]
    def set_tile_w(self,tw):
        self._region[2] = tw
        self._gen_subs()
    tile_w = property(get_tile_w,set_tile_w,None,
        """ Width of all subsurfaces.
            Changing this will update the list of subsurfaces.
        """)
    
    def get_tile_h(self):
        return self._region[3]
    def set_tile_h(self,th):
        self._region[3] = th
        self._gen_subs()
    tile_h = property(get_tile_h,set_tile_h,None,
        """ Height of all subsurfaces.
            Changing this will update the list of subsurfaces.
        """)

    # TODO: Properties for changing pairs of or all of the dimensions at once
    
    def get_subs(self):
        return self._sub
    subs = property(get_subs,None,None,
        " Tuple of all subsurfaces of the current shared surface. (Read only.) ")



def from_json(obj, resmgr=None):
    """ Generic TileList JSON decoder.
    
        obj:    JSON rep of TileList object.
        resmgr: Resource manager to load dependencies.
        
        Returns a TileList-compatible object.
        
        TODO: Add support for TileSheets.
    """
    return {"tilelist":TileList}[obj["tag"]].from_json(obj, resmgr)

def load(src, resmgr=None):
    """ Load a TileList from a file.
    
        src:    File or file name to load.
        
        resmgr: A resource manager to use to load references resources.
                If None, the global resource manager is used.
        
        returns a TileList object.
        
        TODO: Add support for loading TileSheets as well.
    """
    
    # Determine the correct way to use `src`.
    close = False
    try:
        if isinstance(src, basestring):
            src = open(src)
            close = True
        
        # Load the JSON data from the file.
        obj = json.load(src)
    
    # Close the file if necessary.
    finally:
        if close:
            src.close()
    
    # Turn the JSON object into a TileList.
    obj = from_json(obj, resmgr)
    
    return obj